/**
* Copyright (C) 2020 Xilinx, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may
* not use this file except in compliance with the License. A copy of the
* License is located at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

#define CL_USE_DEPRECATED_OPENCL_1_2_APIS
#include <CL/cl.h>

#include <cstdio>
#include <fstream>
#include <iosfwd>
#include <string>
#include <vector>

using std::ifstream;
using std::ios;
using std::streamsize;
using std::string;
using std::vector;

#define ERROR_CASE(err) \
    case err:           \
        return #err;    \
        break

const char* error_string(cl_int error_code) {
    switch (error_code) {
        ERROR_CASE(CL_SUCCESS);
        ERROR_CASE(CL_DEVICE_NOT_FOUND);
        ERROR_CASE(CL_DEVICE_NOT_AVAILABLE);
        ERROR_CASE(CL_COMPILER_NOT_AVAILABLE);
        ERROR_CASE(CL_MEM_OBJECT_ALLOCATION_FAILURE);
        ERROR_CASE(CL_OUT_OF_RESOURCES);
        ERROR_CASE(CL_OUT_OF_HOST_MEMORY);
        ERROR_CASE(CL_PROFILING_INFO_NOT_AVAILABLE);
        ERROR_CASE(CL_MEM_COPY_OVERLAP);
        ERROR_CASE(CL_IMAGE_FORMAT_MISMATCH);
        ERROR_CASE(CL_IMAGE_FORMAT_NOT_SUPPORTED);
        ERROR_CASE(CL_BUILD_PROGRAM_FAILURE);
        ERROR_CASE(CL_MAP_FAILURE);
        ERROR_CASE(CL_INVALID_VALUE);
        ERROR_CASE(CL_INVALID_DEVICE_TYPE);
        ERROR_CASE(CL_INVALID_PLATFORM);
        ERROR_CASE(CL_INVALID_DEVICE);
        ERROR_CASE(CL_INVALID_CONTEXT);
        ERROR_CASE(CL_INVALID_QUEUE_PROPERTIES);
        ERROR_CASE(CL_INVALID_COMMAND_QUEUE);
        ERROR_CASE(CL_INVALID_HOST_PTR);
        ERROR_CASE(CL_INVALID_MEM_OBJECT);
        ERROR_CASE(CL_INVALID_IMAGE_FORMAT_DESCRIPTOR);
        ERROR_CASE(CL_INVALID_IMAGE_SIZE);
        ERROR_CASE(CL_INVALID_SAMPLER);
        ERROR_CASE(CL_INVALID_BINARY);
        ERROR_CASE(CL_INVALID_BUILD_OPTIONS);
        ERROR_CASE(CL_INVALID_PROGRAM);
        ERROR_CASE(CL_INVALID_PROGRAM_EXECUTABLE);
        ERROR_CASE(CL_INVALID_KERNEL_NAME);
        ERROR_CASE(CL_INVALID_KERNEL_DEFINITION);
        ERROR_CASE(CL_INVALID_KERNEL);
        ERROR_CASE(CL_INVALID_ARG_INDEX);
        ERROR_CASE(CL_INVALID_ARG_VALUE);
        ERROR_CASE(CL_INVALID_ARG_SIZE);
        ERROR_CASE(CL_INVALID_KERNEL_ARGS);
        ERROR_CASE(CL_INVALID_WORK_DIMENSION);
        ERROR_CASE(CL_INVALID_WORK_GROUP_SIZE);
        ERROR_CASE(CL_INVALID_WORK_ITEM_SIZE);
        ERROR_CASE(CL_INVALID_GLOBAL_OFFSET);
        ERROR_CASE(CL_INVALID_EVENT_WAIT_LIST);
        ERROR_CASE(CL_INVALID_EVENT);
        ERROR_CASE(CL_INVALID_OPERATION);
        ERROR_CASE(CL_INVALID_GL_OBJECT);
        ERROR_CASE(CL_INVALID_BUFFER_SIZE);
        ERROR_CASE(CL_INVALID_MIP_LEVEL);
        ERROR_CASE(CL_INVALID_GLOBAL_WORK_SIZE);
        ERROR_CASE(CL_COMPILE_PROGRAM_FAILURE);
        ERROR_CASE(CL_LINKER_NOT_AVAILABLE);
        ERROR_CASE(CL_LINK_PROGRAM_FAILURE);
        ERROR_CASE(CL_DEVICE_PARTITION_FAILED);
        ERROR_CASE(CL_KERNEL_ARG_INFO_NOT_AVAILABLE);
        ERROR_CASE(CL_INVALID_PROPERTY);
        ERROR_CASE(CL_INVALID_IMAGE_DESCRIPTOR);
        ERROR_CASE(CL_INVALID_COMPILER_OPTIONS);
        ERROR_CASE(CL_INVALID_LINKER_OPTIONS);
        ERROR_CASE(CL_INVALID_DEVICE_PARTITION_COUNT);
        default:
            printf("Unknown OpenCL Error (%d)\n", error_code);
            break;
    }
    return nullptr;
}

static const char* error_message =
    "Error: Result mismatch:\n"
    "i = %d CPU result = %d Device result = %d\n";

vector<unsigned char> readBinary(const std::string& fileName) {
    ifstream file(fileName, ios::binary | ios::ate);
    if (file) {
        file.seekg(0, ios::end);
        streamsize size = file.tellg();
        file.seekg(0, ios::beg);
        vector<unsigned char> buffer(size);
        file.read((char*)buffer.data(), size);
        return buffer;
    } else {
        return std::vector<unsigned char>(0);
    }
}

// This example just prints out string description for given OpenCL error code.
int main(int argc, char** argv) {
    if (argc != 2) {
        printf("Usage: %s <XCLBIN File>\n", argv[0]);
        return EXIT_FAILURE;
    }
    static const int elements = 1024;
    char* binary_file_path = argv[1];

    // Error handling in OpenCL is performed using the cl_int specifier. OpenCL
    // functions either return or accept pointers to cl_int types to indicate if
    // an error occurred.
    cl_int err;

    if ((err = clGetPlatformIDs(0, nullptr, nullptr))) {
        printf(
            "Received Expected Error calling clGetPlatformIDs: %s\n"
            "\tThis error is usually caused by a failed OpenCL installation or "
            "if both the platforms and num_platforms parameters are null. In "
            "this case we passed incorrect values to the function. We "
            "intentionally threw this error so we will not be exiting the "
            "program. \n\n",
            error_string(err));
        // Normally you would exit the program if this call returned an error
        // but in
        // this case we will continue processing after fixing the command
        // exit(EXIT_FAILURE);
    }

    cl_uint num_platforms;
    if ((err = clGetPlatformIDs(0, nullptr, &num_platforms))) {
        printf(
            "Fatal Error calling clGetPlatformIDs: %s\n"
            "This can be caused by an invalid installation of the OpenCL "
            "runtime. Please make sure the installation was successful.\n",
            error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    if (num_platforms == 0) {
        printf(
            "No platforms were found. This could be caused because the OpenCL "
            "icd was not installed in the /etc/OpenCL/vendors directory.\n");
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    vector<cl_platform_id> platforms(num_platforms + 1);
    if ((err = clGetPlatformIDs(num_platforms, platforms.data(), nullptr))) {
        printf("Error: Failed to find an OpenCL platform!\n");
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    string platform_name(1024, '\0');
    size_t actual_size = 0;
    if ((err = clGetPlatformInfo(platforms[0], CL_PLATFORM_NAME, platform_name.size(), (void*)platform_name.data(),
                                 &actual_size))) {
        printf("Error: Could not determine platform name!\n");
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }
    printf("Platform Name: %s\n", platform_name.c_str());

    cl_uint num_devices = 0;
    if ((err = clGetDeviceIDs(platforms[0], CL_DEVICE_TYPE_CPU, 0, nullptr, &num_devices))) {
        printf(
            "Received Expected Error calling clGetDeviceIDs: %s\n"
            "This error appears when we try to create a device and no devices "
            "are found on the platform. In this case we passed "
            "CL_DEVICE_TYPE_CPU as the device type which is not available on "
            "the %s platform. We intentionally threw this error so we will not "
            "be exiting the program.\n\n",
            error_string(err), platform_name.c_str());
    }
    cl_device_id device_id = 0;
    if ((err = clGetDeviceIDs(platforms[0], CL_DEVICE_TYPE_ALL, 1, &device_id, nullptr))) {
        printf(
            "Fatal Error calling clGetDeviceIDs: %s\n"
            "Unexpected error getting device IDs. This may happen if you are "
            "Targeting hardware or software emulation and the "
            "XCL_EMULATION_MODE environment variable is not set. Also makeyou "
            "have set the you have run the emconfigutil to setup the emulation "
            "environment.\n\n",
            error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    cl_context_properties props[3] = {CL_CONTEXT_PLATFORM, (cl_context_properties)platforms[0], 0};
    // clCreate* function calls return the object so they return error codes
    // using
    // pointers to cl_int as their last parameters
    cl_context context = clCreateContext(props, 0, &device_id, nullptr, nullptr, &err);
    if (err) {
        printf(
            "Received Expected Error calling clCreateContext: %s\n"
            "\tMost clCreate* calls accept error codes as their last parameter "
            "instead of returning the error value. This error occurred because "
            "we passed 0 for the num_devices variable. We intentionally threw "
            "this error so we will not be exiting the program.\n\n",
            error_string(err));
    }

    context = clCreateContext(props, 1, &device_id, nullptr, nullptr, &err);

    cl_command_queue command_queue = clCreateCommandQueue(context, device_id, CL_QUEUE_PROFILING_ENABLE, &err);
    if (err) {
        printf(
            "Fatal Error calling clCreateCommandQueue: %s\n"
            "Unexpected error creating a command queue.\n\n",
            error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    // Loading the file
    std::string wrong_binary_file_path = "XYZ";
    vector<unsigned char> incorrect_binary = readBinary(wrong_binary_file_path);
    size_t binary_size = incorrect_binary.size();
    const unsigned char* incorrect_binary_data = incorrect_binary.data();
    cl_program program =
        clCreateProgramWithBinary(context, 1, &device_id, &binary_size, &incorrect_binary_data, nullptr, &err);
    if (err) {
        printf(
            "Received Expected Error calling clCreateProgramWithBinary: %s\n"
            "Errors caused during program creation are usually due to invalid "
            "binaries. The binary may be targeting a different shell. "
            "It may also have been corrupted or incorrectly read from disk. We "
            "intentionally caused this error so we will not be exiting the "
            "program.\n\n",
            error_string(err));
    }

    std::vector<unsigned char> binary = readBinary(binary_file_path);
    binary_size = binary.size();
    const unsigned char* binary_data = binary.data();

    program = clCreateProgramWithBinary(context, 1, &device_id, &binary_size, &binary_data, nullptr, &err);
    if (err) {
        printf(
            "Fatal Error calling clCreateProgramWithBinary: %s\n"
            "Unexpected error creating a program from binary. Make sure you "
            "executed this program with the correct path to the binary.\n\n",
            error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    cl_kernel kernel = clCreateKernel(program, "InvalidKernelName", &err);
    if (err) {
        printf(
            "Received Expected Error calling clCreateKernel: %s\n"
            "Errors calling clCreateKernel are usually caused if the name "
            "passed into the function does not match a kernel in the binary. "
            "We intentionally caused this error so we will not be exiting the "
            "program.\n\n",
            error_string(err));
    }

    kernel = clCreateKernel(program, "vector_add", &err);
    if (err) {
        printf(
            "Fatal Error calling clCreateKernel: %s\n"
            "Unexpected error when creating kernel. Make sure you passed the "
            "correct binary into the executable of this program.\n\n",
            error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    cl_mem buffer_a = clCreateBuffer(context, CL_MEM_READ_ONLY, 0, nullptr, &err);
    if (err) {
        printf(
            "Received Expected Error calling clCreateBuffer: %s\n"
            "There can be several reasons for buffer creation to fail. It "
            "could be because device could not allocate enough memory for this "
            "buffer. The pointer could be null and either CL_MEM_USE_HOST_PTR "
            "or CL_MEM_COPY_HOST_PTR are passed into the flags parameter. In "
            "this case we passed zero(0) as the size of the buffer. We "
            "intentionally caused this error so we will not be exiting the "
            "program.\n\n",
            error_string(err));
    }

    size_t size = elements * sizeof(int);
    buffer_a = clCreateBuffer(context, CL_MEM_READ_ONLY, size, nullptr, &err);
    if (err) {
        printf("Fatal Error calling clCreateBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }
    cl_mem buffer_b = clCreateBuffer(context, CL_MEM_READ_ONLY, size, nullptr, &err);
    if (err) {
        printf("Fatal Error calling clCreateBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }
    cl_mem buffer_result = clCreateBuffer(context, CL_MEM_WRITE_ONLY, size, nullptr, &err);
    if (err) {
        printf("Fatal Error calling clCreateBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    vector<int> A(elements, 32);
    vector<int> B(elements, 10);
    vector<int> C(elements);

    if ((err = clSetKernelArg(kernel, 0, sizeof(cl_mem), &buffer_result))) {
        printf("Fatal Error calling clSetKernelArg: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }
    if ((err = clSetKernelArg(kernel, 1, sizeof(cl_mem), &buffer_a))) {
        printf("Fatal Error calling clSetKernelArg: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }
    if ((err = clSetKernelArg(kernel, 2, sizeof(cl_mem), &buffer_b))) {
        printf("Fatal Error calling clSetKernelArg: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    if ((err = clSetKernelArg(kernel, 3, sizeof(int), &elements))) {
        printf("Fatal Error calling clSetKernelArg: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    if ((err = clEnqueueWriteBuffer(command_queue, buffer_a, CL_FALSE, 0, size + 1, A.data(), 0, nullptr, nullptr))) {
        printf(
            "Received Expected Error calling clEnqueueWriteBuffer: %s\n"
            "Errors calling clEnqueueWriteBuffer tend to occur due to invalid "
            "pointers or invalid size of the transfer. Make sure that the host "
            "pointer is correct and that you are transferring less than the "
            "size of the buffer. Here we tried to transfer data that was "
            "larger than the size of the buffer. We intentionally caused this "
            "error so we will not be exiting the program.\n\n",
            error_string(err));
    }

    if ((err = clEnqueueWriteBuffer(command_queue, buffer_a, CL_FALSE, 0, size, A.data(), 0, nullptr, nullptr))) {
        printf("Fatal Error calling clEnqueueWriteBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    if ((err = clEnqueueWriteBuffer(command_queue, buffer_b, CL_FALSE, 0, size, B.data(), 0, nullptr, nullptr))) {
        printf("Fatal Error calling clEnqueueWriteBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    size_t global = 1;
    size_t local = 1;
    if ((err = clEnqueueNDRangeKernel(command_queue, kernel, 1, nullptr, &global, &local, 0, nullptr, nullptr))) {
        printf("Fatal Error calling clEnqueueNDRangeKernel: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    if ((err = clEnqueueReadBuffer(command_queue, buffer_result, CL_TRUE, 0, size, C.data(), 0, nullptr, nullptr))) {
        printf("Fatal Error calling clEnqueueWriteBuffer: %s\n", error_string(err));
        printf("TEST FAILED\n");
        exit(EXIT_FAILURE);
    }

    int failed = 0;
    for (int i = 0; i < elements; i++) {
        int host_result = A[i] + B[i];
        if (C[i] != host_result) {
            printf(error_message, i, host_result, C[i]);
            failed = 1;
            break;
        }
    }

    clReleaseMemObject(buffer_a);
    clReleaseMemObject(buffer_b);
    clReleaseMemObject(buffer_result);
    clReleaseKernel(kernel);
    clReleaseProgram(program);
    clReleaseCommandQueue(command_queue);
    clReleaseContext(context);

    printf("TEST %s\n", (failed ? "FAILED" : "PASSED"));
    return (failed ? EXIT_FAILURE : EXIT_SUCCESS);
}
